using System;
using System.Collections.Generic;
using System.Linq;
using Sustainalytics.ClientDW.Entities;
using Sustainalytics.DataLayer.Exceptions;
using Sustainalytics.DataLayer.Shared;
using Sustainalytics.Entities;
using Sustainalytics.Portfolios.BL.Exceptions;
using Sustainalytics.Portfolios.BL.Model;
using Sustainalytics.Portfolios.BL.Validators;
using Sustainalytics.Utils;

namespace Sustainalytics.Portfolios.BL
{
    using global::MongoDB.Bson;
    using global::MongoDB.Driver;
    using MongoDB.Extensions;
    using System.Linq.Expressions;

    public class PortfolioReadHandler : IPortfolioReadHandler
    {
        private readonly IRepositoryCollection<PortfolioDetails> _portfolioDetailsRepository;

        private readonly IRepositoryCollection<Portfolio> _portfolioRepository;

        private readonly PortfolioDataSourceStrategy _strategy = new PortfolioDataSourceStrategy();

        private readonly IRepositoryCollection<PortfolioDetails> _systemPortfolioDetailsRepository;

        private readonly IRepositoryCollection<Portfolio> _systemPortfolioRepository;

        private readonly IRepositoryCollection<CorporateDataBasic> _universeRepository;
        public PortfolioReadHandler(
            PortfolioTypeCollection<Portfolio> portfolioCollections,
            PortfolioTypeCollection<PortfolioDetails> portfolioDetailsCollection,
            IRepositoryCollection<CorporateDataBasic> universeRepository
           )
        {
            _portfolioRepository = portfolioCollections.ClientCollection;
            _portfolioDetailsRepository = portfolioDetailsCollection.ClientCollection;
            _universeRepository = universeRepository;
            _systemPortfolioRepository = portfolioCollections.SystemCollections;
            _systemPortfolioDetailsRepository = portfolioDetailsCollection.SystemCollections;
            _strategy.RegisterRepository(typeof(SystemPortfolio), _systemPortfolioRepository, _systemPortfolioDetailsRepository);
            _strategy.RegisterRepository(typeof(Portfolio), _portfolioRepository, _portfolioDetailsRepository);
        }

        public IList<Guid> GetCompanyIdsByPortfolio(Guid portfolioId)
        {
            var companies = _portfolioDetailsRepository.ReadWhere(x => x.PortfolioId == portfolioId, null, null, null, x => x.CompanyId);

            if (companies.Any())
            {
                return companies.Select(x => x.CompanyId).Distinct().ToList();
            }

            var systemPortfolio = _systemPortfolioRepository.Read(portfolioId) as SystemPortfolio;

            if (systemPortfolio == null)
            {
                return new List<Guid>();
            }

            var portfolioIds = systemPortfolio.RelatedUniverses.Select(x => x.Id).ToArray();

            var systemPortfoliosCompanies = _systemPortfolioDetailsRepository.ReadWhere(p => portfolioIds.Contains(p.PortfolioId), null, null, null, x => x.CompanyId);

            if (systemPortfoliosCompanies.Any())
            {
                return systemPortfoliosCompanies.Select(x => x.CompanyId).Distinct().ToList();
            }

            return new List<Guid>();
        }

        public string GetPortfolioNameById(Guid id)
        {
            var portfolio = _portfolioRepository.Read(id) ?? _systemPortfolioRepository.Read(id);

            if (portfolio == null)
            {
                throw new PortfolioNotFoundException("Portfolio not found", id);
            }

            return portfolio.Name;
        }


        public Portfolio GetPortfolioById(Guid id, PortfolioUserClaims userClaims = null)
        {
            var portfolio = _portfolioRepository.Read(id) ?? _systemPortfolioRepository.Read(id);

            if (portfolio == null)
            {
                throw new PortfolioNotFoundException("Portfolio not found", id);
            }

            if (userClaims != null && !PortfolioValidator.Read().IsValid(portfolio, userClaims))
            {
                throw new PortfolioGetUnauthorizedException();
            }

            return portfolio;
        }

        public IList<Portfolio> GetPortfolios(PortfolioFilter filter, int? skip = null, int? take = null, string[] orderBy = null, bool orderByAsc = true)
        {
            var argsStr = LogArgument.GetString(new { filter, skip, take, orderBy });
            try
            {
                PortfolioManagementEventSource.Log.GetPortfolios(argsStr);

                var portofolioOrdering = GetPortofolioOrdering(orderByAsc);

                List<Portfolio> portfolioList = new List<Portfolio>();

                foreach (var repository in _strategy.GetPortfolioRepositories(filter.PortfolioType))
                {
                    var items = repository.ReadWhere(filter.GetFilterExpression(), skip, take, portofolioOrdering);
                    portfolioList.AddRange(items);
                }

                PortfolioManagementEventSource.Log.GetPortfoliosSuccess(argsStr);

                return portfolioList;
            }
            catch (Exception ex)
            {
                PortfolioManagementEventSource.Log.GetPortfoliosError(argsStr, ex);
                throw;
            }
        }

        public IList<Portfolio> GetPortfoliosByCompanyId(Guid companyId, int? skip = null, int? take = null, string[] orderBy = null, bool orderByAsc = true)
        {
            var portfolioDetails = _portfolioDetailsRepository.ReadWhere(x => x.CompanyId == companyId, null, null, null, x => x.PortfolioId);
            var portFolioIds = portfolioDetails.Select(x => x.PortfolioId).Distinct().ToList();

            return _portfolioRepository.ReadWhere(x => portFolioIds.Contains(x.Id));
        }

        public IList<Portfolio> GetPortfoliosByIds(ICollection<Guid> ids, PortfolioUserClaims userClaims)
        {
            if (userClaims.UserId == default(Guid))
            {
                throw new PortfolioUserIdClaimNull();
            }

            if (userClaims.AccountId == default(Guid))
            {
                throw new PortfolioAccountIdClaimNull();
            }

            List<Portfolio> portfolios = new List<Portfolio>();

            foreach (var repository in _strategy.GetPortfolioRepositories(PortfolioTypes.All))
            {
                var items = repository.ReadWhere(x => ids.Contains(x.Id));
                portfolios.AddRange(items);
            }

            if (!portfolios.Any())
            {
                throw new PortfolioNotFoundException();
            }

            if (portfolios.Any(portfolio => !PortfolioValidator.Read().IsValid(portfolio, userClaims)))
            {
                throw new PortfolioGetUnauthorizedException();
            }

            return portfolios;
        }

        public long GetPortfoliosCount(PortfolioFilter filter)
        {
            return
                _strategy.GetPortfolioRepositories(filter.PortfolioType)
                    .Sum(repository => repository.ReadCount(filter.GetFilterExpression()));
        }

        public IList<PortfolioSecurityModel> GetPortfolioSecurities(
            Guid portfolioId,
            PortfolioUserClaims userClaims = null,
            int? skip = null,
            int? take = null,
            string[] orderBy = null,
            bool orderByAsc = true)
        {
            int totalCount;
            return GetPortfolioSecurities(portfolioId, out totalCount, userClaims, skip, take, orderBy, orderByAsc);
        }

        public List<PortfolioSecurityModel> GetPortfolioSecurities(Guid portfolioId, List<int> universe)
        {
            var details = new List<PortfolioDetails>();
            var portfolio = GetPortfolioById(portfolioId);

            var portfolioDetailsRepositories = _strategy.GetPortfolioDetailsRepositories(portfolio.IsSystemPortfolio() ? PortfolioTypes.System : PortfolioTypes.Custom).First();

            var readingExpression = GetReadingExpression(portfolio);

            details.AddRange(portfolioDetailsRepositories.ReadAllWhere(readingExpression));
            return GetPortfolioSecurities(details, universe, true);
        }

        public IList<PortfolioSecurityModel> GetPortfolioSecurities(
            Guid portfolioId,
            out int totalCount,
            PortfolioUserClaims userClaims,
            int? skip = null,
            int? take = null,
            string[] orderBy = null,
            bool orderByAsc = true)
        {
            if (_universeRepository == null)
            {
                throw new NullReferenceException("_universesRepository");
            }

            var securities = new List<PortfolioDetails>();
            var portfolio = GetPortfolioById(portfolioId, userClaims);
            totalCount = portfolio.CompanyCount;

            var orderingDetails = OrderingDetails(orderBy, orderByAsc);

            var portfolioDetailsRepositories =
                _strategy.GetPortfolioDetailsRepositories(
                    portfolio.IsSystemPortfolio() ? PortfolioTypes.System : PortfolioTypes.Custom).First();

            var readingExpression = GetReadingExpression(portfolio);

            securities.AddRange(
                    portfolioDetailsRepositories.ReadWhere(readingExpression, skip, take, orderingDetails));

            return GetPortfolioSecurities(securities, userClaims.Universes, true);
        }


        public IList<PortfolioSecurityModel> GetMatchedPortfolioSecurities(
                                                                string portfolioName,
                                                                Guid userId,
                                                                out long totalCount,
                                                                ListOptions listOptions,
                                                                PortfolioUserClaims userClaims)
        {
            if (this._universeRepository == null)
            {
                throw new NullReferenceException("_universesRepository");
            }

            totalCount = MongoFactory.GetCollectionFromClientPlatform<PortfolioMatches>().Count(x => x.PortfolioName == portfolioName && x.Added && x.AddedByUserId == userId);

            var order = listOptions.OrderAscending ? Builders<PortfolioMatches>.Sort.Ascending(listOptions.OrderBy.FirstOrDefault())
                                   : Builders<PortfolioMatches>.Sort.Descending(listOptions.OrderBy.FirstOrDefault());
            var portfolioMatches = MongoFactory.GetCollectionFromClientPlatform<PortfolioMatches>().Find(x => x.PortfolioName == portfolioName && x.Added && x.AddedByUserId == userId)
                                                            .Sort(order)
                                                            .Limit(listOptions.Take)
                                                            .Skip(listOptions.Skip)
                                                            .ToList();

            return portfolioMatches.Select(x => {
                var security = new PortfolioSecurityModel
                {
                    PortfolioId = x.PortfolioId,
                    DisplayInfo = x.DisplayInfo,
                    MatchedInfo = x.MatchedInfo,
                    CompanyId = x.CompanyId,
                    CompanyNameUrlFriendly = x.CompanyNameUrlFriendly,
                    SecurityId = x.SecurityId,
                    Id = x.Id,
                    SecurityWeight = x.SecurityWeight,
                };

                return security;
            }).ToList();
        }

        public IEnumerable<object> GetUnmatchedPortfolioSecurities(
                                                                string portfolioName,
                                                                Guid userId,
                                                                out long totalCount,
                                                                ListOptions listOptions,
                                                                PortfolioUserClaims userClaims)
        {
            if (this._universeRepository == null)
            {
                throw new NullReferenceException("_universesRepository");
            }

            totalCount = MongoFactory.GetCollectionFromClientPlatform<PortfolioMatches>().Count(x => x.PortfolioName == portfolioName && !x.Added && x.AddedByUserId == userId);

            var order = listOptions.OrderAscending ? Builders<PortfolioMatches>.Sort.Ascending(listOptions.OrderBy.FirstOrDefault())
                                   : Builders<PortfolioMatches>.Sort.Descending(listOptions.OrderBy.FirstOrDefault());
            var portfolioMatches = MongoFactory.GetCollectionFromClientPlatform<PortfolioMatches>().Find(x => x.PortfolioName == portfolioName && !x.Added && x.AddedByUserId == userId)
                                                            .Sort(order)
                                                            .Limit(listOptions.Take)
                                                            .Skip(listOptions.Skip)
                                                            .ToList();
            return portfolioMatches.GroupBy(p => p.GroupId).Select(x =>
            {
                return new
                {
                    PortfolioId = x.First().PortfolioId,
                    DisplayInfo = x.First().DisplayInfo,
                    MatchedInfo = x.Select(pm => new
                    {
                        CapitalIQId = pm.MatchedInfo.CapitalIQId,
                        CompanyName = pm.MatchedInfo.CompanyName,
                        PortfolioMatchId = pm.Id,
                        MatchType = pm.MatchType
                    }).ToList(),
                    CompanyId = x.First().CompanyId,
                    Id = x.First().Id,
                    SecurityWeight = x.First().SecurityWeight,
                    GroupId = x.First().GroupId
                };
            });
        }

        private static Ordering<Portfolio> GetPortofolioOrdering(bool orderByAsc)
        {
            var portofolioOrdering = orderByAsc
                                         ? Ordering<Portfolio>.Ascending(x => x.NameLowercase)
                                         : Ordering<Portfolio>.Descending(x => x.NameLowercase);
            return portofolioOrdering;
        }

        private static Expression<Func<PortfolioDetails, bool>> GetReadingExpression(Portfolio portfolio)
        {
            // Added a check against Guid.Empty in order to filter the unmatched securities from portfolio details; Added a check for invalid match
            Expression<Func<PortfolioDetails, bool>> readingExpression = p => p.PortfolioId == portfolio.Id && p.SecurityId!= null && p.CompanyId != Guid.Empty && !p.IsInvalidMatch;
            if (!portfolio.IsSystemPortfolio())
            {
                return readingExpression;
            }

            var systemPortfolio = portfolio as SystemPortfolio;
            if (systemPortfolio != null)
            {
                var portfolioIds = systemPortfolio.RelatedUniverses.Select(x => x.Id).ToArray();
                readingExpression = p => portfolioIds.Contains(p.PortfolioId);
            }

            return readingExpression;
        }

        private static Ordering<PortfolioDetails> OrderingDetails(string[] orderBy, bool orderByAsc)
        {
            return orderBy != null ? OrderBy<PortfolioDetails>(orderBy, orderByAsc)
                                   : Ordering<PortfolioDetails>.Ascending(x => x.CompanyNameLowercase);
        }

        private static Ordering<T> OrderBy<T>(string[] orderBy, bool orderByAsc)
        {
            return orderByAsc ? Ordering<T>.Ascending(
                                orderBy.FirstOrDefault().GetExpression<T>())
                                : Ordering<T>.Descending(
                                orderBy.FirstOrDefault().GetExpression<T>());
        }

        private List<PortfolioSecurityModel> GetPortfolioSecurities(
            ICollection<PortfolioDetails> details,
            ICollection<int> universes,
            bool all)
        {
            ISecurityModelFactory factory = new SecurityModelFactory();
            var companyUniverses = this.GetUniversesBySecurities(details);
            List<PortfolioSecurityModel> result = new List<PortfolioSecurityModel>();

            foreach (var portfolioDetails in details)
            {
                var found = companyUniverses.FirstOrDefault(a => a.Item1 == portfolioDetails.CompanyId);
                if (found != null)
                {
                    var security = factory.Create(portfolioDetails, found.Item2);
                    security.ValidateAgainstUserUniverses(universes);
                    if (all || security.IsAccesible)
                    {
                        result.Add(security);
                    }
                }
            }

            return result;
        }

        private List<Tuple<Guid, List<int>>> GetUniversesBySecurities(ICollection<PortfolioDetails> securities)
        {
            var companyIds = securities.Select(x => x.CompanyId).Distinct();
            return _universeRepository.ReadWhere(x => companyIds.Contains(x.Id), null, null, null, x => x.Id, x => x.UniverseIds).Select(x => new Tuple<Guid, List<int>>(x.Id, x.UniverseIds)).ToList();
        }
    }
}